import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.RemoteServer;
import java.rmi.server.ServerNotActiveException;
import java.rmi.server.UnicastRemoteObject;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Random;
import java.util.Timer;
import java.util.TimerTask;

public class GameBoard  implements GameboardInterface {

	static String refGameBoard = "GameBoard";
	static final int WAITING_TIME = 20;
	
	// error codes
	static String currentPositionNotFound  = " Current position of the player cannot be found. ";
	static String positionOutOfBounds  = " Move invalid, current move will take you out of map boundary. ";
	static String playerExistInLocation  = " Move invalid, a player alredy exists in the requested location. ";
	static String playerNotRegistered  = " You are not a registered player for this game. ";
	static String playerDroppedWithoutNotifying  = " Player Droppped from game without notifying. ";
	static String gameNotStartedMoveDenied  = " Game has not yet been started. Request to move denied. ";
	static String gameNotStartedDisconnectDenied  = " Game has not yet been started. Request to disconnect player denied. ";
	
	
	public HashMap <Integer, Integer> playerTreasureCount;
	public HashMap <Integer, Coordinates> playerMapCoordinates;
	public HashMap <Integer, HashMap<Integer, Integer>> remainingTreasureCoordinates;
	public HashMap <Integer, GamePlayerInterface> players; 
	
	public static int mapSize;
	public static int treasure;
	
	public int gameStartTime;
	public int gameStartRemTime;
	public int lastPlayerId;
	public GameStatusInfo gameStatusInfo;
	private TimerTask timertask;
	private Timer timer;
	private int remainingTreasure;
	int updateTime;
	
	public void start() {
				
		int serverPort=30416;
		GameboardInterface stub;
		Registry gameServerRegistry=null;
		
		try {
			LocateRegistry.createRegistry(serverPort);
			stub = (GameboardInterface) UnicastRemoteObject.exportObject(this, 0);
			gameServerRegistry = LocateRegistry.getRegistry(serverPort);
			gameServerRegistry.bind(refGameBoard, stub);
			this.printStart();			
		}

		catch (Exception ex) {
			System.out.println("GameBoard Exception: " + ex.getMessage());
			ex.printStackTrace();
		}
	}
	
	/*
	 * Constructor
	 */
	public GameBoard(int n, int m){
		mapSize = n;
		treasure = m;
		gameStatusInfo = new GameStatusInfo();
		gameStatusInfo.SetGameStatus(GameStatusInfo.GameStatus.GAME_IDLE);
		updateTime=1000;
	}
	
	private void initGameBoard() {
		timertask = new GameTimeScheduler(this);
		timer = new Timer();
		timer.schedule(timertask, 1000, updateTime);
		remainingTreasure=treasure;
		gameStartTime=WAITING_TIME;
		gameStartRemTime=gameStartTime;
		lastPlayerId=0;
		
		playerTreasureCount = new HashMap<Integer, Integer>();
		playerMapCoordinates = new HashMap<Integer, Coordinates>();		
		remainingTreasureCoordinates = new HashMap<Integer, HashMap<Integer, Integer>>();
		players = new HashMap <Integer, GamePlayerInterface> ();
	}


	/*
	 * add new player to map, and assign playerID to treasure count map
	 */
	private void AddPlayer(GamePlayerInterface gamePlayerInteface) {
		
		Coordinates coordinates;
			
		// add player ID to playerTreasureCount HashMap
		playerTreasureCount.put(lastPlayerId,0);
		
		// add player to the map
		// assign players to random locations in the map
		Random random = new Random();
				
		while (true){
			coordinates = new Coordinates(random.nextInt(mapSize), random.nextInt(mapSize));
			// if another player doesn't reside place player to that location
			if (!playerMapCoordinates.containsValue(coordinates)){
				playerMapCoordinates.put(lastPlayerId, coordinates);
				break;
			}
		}
		
		// add players to playerInfoList
		players.put(lastPlayerId, gamePlayerInteface);
		System.out.println("GameBoard message: New Player Joined ~ Player ID: " + lastPlayerId + " Total Players: " + players.size());
		System.out.println("GameBoard message: Player placed ~ Player id: " + lastPlayerId + " Coordinates: " + coordinates.displayCoordinates());
	}

	@Override
	public synchronized GameStatusInfo ConnectPlayer (GamePlayerInterface gamePlayerInteface) throws RemoteException {

		int tmpLastPlayerID;
		Random random = new Random();
		int xCoordinate, yCoordinate;
		HashMap<Integer, Integer> yCoordinateHashMap;

		// create a new game if GAME_IDLE
		if (gameStatusInfo.GetGameStatus() == GameStatusInfo.GameStatus.GAME_IDLE){
			Coordinates coordinates;
			initGameBoard();

			// Assign treasures in the map at random locations
			
			for (int x=0; x<treasure; x++){
				
				xCoordinate = random.nextInt(mapSize);
				yCoordinate = random.nextInt(mapSize);
				coordinates = new Coordinates(xCoordinate, yCoordinate);
				
				System.out.println("GameBoard message: New treasure ~ Coordinates: " + coordinates.displayCoordinates());

				// if coordinates exist increment the treasure		
				if (remainingTreasureCoordinates.containsKey(xCoordinate)) {
					yCoordinateHashMap = remainingTreasureCoordinates.get(xCoordinate);
					if (yCoordinateHashMap.containsKey(yCoordinate)) {
						yCoordinateHashMap.put(yCoordinate, yCoordinateHashMap.get(yCoordinate)+1);
						remainingTreasureCoordinates.put(xCoordinate, yCoordinateHashMap);
					}
					// else add treasure to new location
					else {
						yCoordinateHashMap.put(yCoordinate, 1);
						remainingTreasureCoordinates.put(xCoordinate, yCoordinateHashMap);
					}
				}
				
				// else add treasure to new location
				else {
					yCoordinateHashMap = new HashMap<Integer, Integer>();
					yCoordinateHashMap.put(yCoordinate, 1);
					remainingTreasureCoordinates.put(xCoordinate, yCoordinateHashMap);
				}
						
			}
			gameStatusInfo.SetGameStatus(GameStatusInfo.GameStatus.GAME_STARTING);
			System.out.println("GameBoard message: New Game ~ Created");
		}

		// player cannot join game if the game has already started
		if (gameStatusInfo.GetGameStatus() == GameStatusInfo.GameStatus.GAME_STARTED){

			gameStatusInfo.SetPlayerID(-1);
			gameStatusInfo.SetStartRemTime(-1);
			return gameStatusInfo;

		}

		// if GameStatus is either GAME_STARTING or GAME_IDLE, proceed
		gameStatusInfo.SetPlayerID(++lastPlayerId);

		// notify the player no map size and treasure count
		gameStatusInfo.SetMapSize(mapSize);
		gameStatusInfo.SetTreasureCount(treasure);

		// gameStart remaing time updated to GameStatusInfo by UpdateGameStartTime()

		// call priate method to insert player to map, and init treasure count
		AddPlayer(gamePlayerInteface);
		
		// get LastPlayer's coordinates
		tmpLastPlayerID = lastPlayerId;
		while (tmpLastPlayerID > 0) {
			if (players.containsKey(tmpLastPlayerID)) {
				gameStatusInfo.SetLastPlayerCoordinate(playerMapCoordinates.get(tmpLastPlayerID));
				break;
			}
			tmpLastPlayerID--;
		}
		return gameStatusInfo;
	}

	@Override
	public synchronized GameStatusInfo DisconnectPlayer(int playerID) throws RemoteException {
		
		// remove player only if game is in GAME_STARTED or GAME_STARTING stage
		if (gameStatusInfo.GetGameStatus() == GameStatusInfo.GameStatus.GAME_STARTED || 
				gameStatusInfo.GetGameStatus() == GameStatusInfo.GameStatus.GAME_STARTING) {
			
			if (players.containsKey(playerID))
				players.remove(playerID);
			
			System.out.println("GameBoard message: Player disconnected ~ Player id: " + playerID + " Total Players: " + players.size());
		}
		else
			System.err.println("GameBoard exception: " + "player Id: "+ playerID + gameNotStartedDisconnectDenied);
			return gameStatusInfo;
		
	}
	
	/*
	 * private method to show the start of game
	 */
	private void printStart() {
		Date dt = new Date();
		DateFormat dtFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
		System.out.println("***************************** Start of Game ************************************");
		System.out.println("****************** Creation Time:  : " + dtFormat.format(dt) + " ***********************");
	}
	
	/*
	 * private method to show the end of game
	 */
	private void printEnd() {
		Date dt = new Date();
		DateFormat dtFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
		System.out.println("***************************** End of Game ************************************");
		System.out.println("*********************** End Time:  : " + dtFormat.format(dt) + " *********************");
	}

	private void printResults () {
		System.out.println("******************************* Results **************************************");
		System.out.println("Player\t\tTreasures");
		
		int tmpPlayerID = lastPlayerId;
		while (tmpPlayerID > 0) {
			if (players.containsKey(tmpPlayerID)){
				System.out.println(tmpPlayerID + "\t\t" + playerTreasureCount.get(tmpPlayerID));
			}
			tmpPlayerID--;
			
		}
	}
	
	@Override
	public synchronized PlayerMovementInfo RequestMove(PlayerMovementInfo packInfo, int PlayerID)
			throws Exception {

		Coordinates coordinates = null;
		Coordinates originalCoordinates = null;
		int remTreasureInCoordinate;
		int tmpPlayerID;
		HashMap<Integer, Integer> yCoordinateHashMap;
		
		//TODO: backup

		System.out.println("-------------------------------------------------------------------------------");
		
		// check if game is started. if not reject move
		if (gameStatusInfo.GetGameStatus() != GameStatusInfo.GameStatus.GAME_STARTED) {
			packInfo.ServerMsg = ("GameBoard exception: " + "player id: " + PlayerID + gameNotStartedMoveDenied);
			packInfo.moveSuccess = false;
			return packInfo;
		}
			
		coordinates = playerMapCoordinates.get(PlayerID);
		if (coordinates == null) {
			System.err.println("GameBoard exception: " + "Player ID: " + PlayerID + "\n" + playerNotRegistered);
			packInfo.ServerMsg = ("GameBoard exception: " + playerNotRegistered);
			packInfo.moveSuccess = false;
			return packInfo;
		}
		
		// check if the move is valid. If invalid dont accept move, return
		System.out.println("Player " + PlayerID + " current coordinates " + coordinates.x + " " + coordinates.y);
		
		// store current location before changing it
		originalCoordinates = new Coordinates(coordinates.x, coordinates.y);
		
		if (packInfo.direction == PlayerMovementInfo.Direction.NO_MOVE) {

			
		} else if ((packInfo.direction == PlayerMovementInfo.Direction.DIR_UP && (coordinates.y-1) < 0) ||
				(packInfo.direction == PlayerMovementInfo.Direction.DIR_DOWN && (coordinates.y+1) >=  mapSize) ||
				(packInfo.direction == PlayerMovementInfo.Direction.DIR_LEFT && (coordinates.x-1) < 0) ||
				(packInfo.direction == PlayerMovementInfo.Direction.DIR_RIGHT && (coordinates.x+1) >= mapSize)) {
			packInfo.ServerMsg = ("GameBoard exception: " + positionOutOfBounds);			
			System.out.println("GameBoard message: Move request denied ~ Player id  " + PlayerID + " Reason: " + positionOutOfBounds);
		} else {			
			
			if (packInfo.direction == PlayerMovementInfo.Direction.DIR_UP)
				--(coordinates.y);
			if (packInfo.direction == PlayerMovementInfo.Direction.DIR_DOWN)
				++(coordinates.y);
			if (packInfo.direction == PlayerMovementInfo.Direction.DIR_LEFT)
				--(coordinates.x);
			if (packInfo.direction == PlayerMovementInfo.Direction.DIR_RIGHT)
				++(coordinates.x);

			// check if another player resides in new location, if exist invalid move return
			
			tmpPlayerID = lastPlayerId;
			while (tmpPlayerID > 0) {
				
				// same player. So will have to cater for no move, exit while and continue
				if (PlayerID == tmpPlayerID) {
					tmpPlayerID--;
					continue;
				}
				
				if (playerMapCoordinates.containsKey(tmpPlayerID))		{
					if (playerMapCoordinates.get(tmpPlayerID).equals(coordinates)) {
						System.out.println("GameBoard message: Move request deined ~ Player id:  " + PlayerID + " Reason: " + playerExistInLocation + " player ID: " + tmpPlayerID);
						
						// restore the original coordinates
						coordinates.CopyCoordinates(originalCoordinates);
						
						// populate PlayerMovementInfo details
						packInfo.ServerMsg = ("GameBoard exception: " + playerExistInLocation);
						packInfo.moveSuccess = false;
						packInfo.playerMapCoordinates = playerMapCoordinates;
						packInfo.playerTreasureCount = playerTreasureCount;
						packInfo.remainingTreasureCoordinates = remainingTreasureCoordinates;
						
						return packInfo;
					}
				}
				tmpPlayerID--;
			}
			
			// move valid
			// update new coordinates of player
			playerMapCoordinates.remove(PlayerID);
			playerMapCoordinates.put(PlayerID, coordinates);
			System.out.println("GameBoard message: New location ~ Player id:  " + PlayerID + " Location: " + coordinates.displayCoordinates());
				
			// check for treasure in new coordinates
	
			if (remainingTreasureCoordinates.containsKey(coordinates.x)) {
				yCoordinateHashMap = remainingTreasureCoordinates.get(coordinates.x);
				
				// tresure found
				if (yCoordinateHashMap.containsKey(coordinates.y)) {
					
					remTreasureInCoordinate = yCoordinateHashMap.get(coordinates.y);
					
					if (remTreasureInCoordinate > 0) {
						yCoordinateHashMap.put(coordinates.y, 0);
						remainingTreasureCoordinates.put(coordinates.x, yCoordinateHashMap);
						remainingTreasure = remainingTreasure-remTreasureInCoordinate;
						
						// add treasure count to player
						playerTreasureCount.put(PlayerID, playerTreasureCount.get(PlayerID)+remTreasureInCoordinate);
						System.out.println("GameBoard message: Treasure Acquired ~ Player id:  " + PlayerID + " No of Treasure: " + playerTreasureCount.get(PlayerID));
					}
				}
			}

		}

		// check if all treasures collected, if so end game
		if (remainingTreasure <= 0) {
			EndGame();
		}
	
		// update values anfd return PlayerMovementInfo
		packInfo.playerMapCoordinates = playerMapCoordinates;
		packInfo.playerTreasureCount = playerTreasureCount;
		packInfo.remainingTreasureCoordinates = remainingTreasureCoordinates;
		packInfo.moveSuccess = true;
		
		return packInfo;
	}

	/*
	 * private method to end game
	 */
	private void EndGame() {
		gameStatusInfo.SetGameStatus(GameStatusInfo.GameStatus.GAME_IDLE);
		// return player tresure count map, to display final results
		gameStatusInfo.playerTreasureCount = playerTreasureCount;
		
		printEnd();	
		
		// notify all player that game has ended
		for (int playerID=1; playerID <= lastPlayerId; playerID++) {
			try {
				// the player might have already been removed
				if (players.containsKey(playerID)) {
					System.out.println("\nSent Game End signal to player " + playerID);
					players.get(playerID).ServerStatusUpdate(gameStatusInfo);
				}
			} catch (RemoteException e) {
				System.err.println("GameBoard exception: " + e.toString());
				System.err.println("GameBoard exception: " + "Player ID: " + playerID + "\n" + playerDroppedWithoutNotifying);
				players.remove(playerID);
			}
		}
		
		// publish final results
		printResults();
		
	}
	
	public void UpdateGameStartTime(){
		
		if (gameStatusInfo.GetGameStatus() == GameStatusInfo.GameStatus.GAME_STARTING) {
			System.out.println("Game start remaining time: " + gameStartRemTime);
			gameStartRemTime--;
			
			gameStatusInfo.SetStartRemTime(gameStartRemTime);
			
			if (gameStartRemTime == 0) {
				gameStatusInfo.SetGameStatus(GameStatusInfo.GameStatus.GAME_STARTED);
				System.out.println("GameBoard message: Game Started");
				
				//Notify all player that game starts
				for (int playerID=1; playerID <= lastPlayerId; playerID++) {
					try {
						// the player might have already been removed
						if (players.containsKey(playerID)) {
							System.out.println("Sent start signal to player " + playerID);
							players.get(playerID).ServerStatusUpdate(gameStatusInfo);
						}
					} catch (RemoteException e) {
						System.err.println("GameBoard exception: " + e.toString());
						System.err.println("GameBoard exception: " + "Player ID: " + playerID + "\n" + playerDroppedWithoutNotifying);
						players.remove(playerID);
					}
				}
			}		
		}
	}
}
